<?php
/**
 * Deferred. Also known as Promise and Futures
 * @link http://javascript.ru/unsorted/async/deferred-deep
 * @link http://twistedmatrix.com/documents/8.1.0/api/twisted.internet.defer.Deferred.html
 * @author Gvozd
 */
class Deferred {

    /**
     * @var array List of callback pairs
     */
    private $callbacks = array();

    /**
     * @var int State of the object
     */
    private $fired = self::NORESULT;
    
    /**
     * States of objects
     */
    const NORESULT = -1;
    const SUCCESS = 0;
    const ERROR = 1;
    /**
     * @var mixed 
     */
    private $result = NULL;

    /**
     * @param callback $callback Callback
     */
    final public function addCallback($callback) {
        if (!is_callable($callback)) {
            throw new DieException('The contents of a variable can not be called as a function');
        }
        $this->callbacks[] = array($callback, NULL);
        if (self::NORESULT !== $this->fired) {
            $this->process($this->result);
        }
    }

    /**
     * @param callback $errback Error Callback
     */
    final public function addErrback($errback) {
        if (!is_callable($errback)) {
            throw new DieException('The contents of a variable can not be called as a function');
        }
        $this->callbacks[] = array(NULL, $errback);
        if (self::NORESULT !== $this->fired) {
            $this->process($this->result);
        }
    }

    /**
     * @param callback $callback Callback
     */
    final public function addBoth($callback) {
        if (!is_callable($callback)) {
            throw new DieException('The contents of a variable can not be called as a function');
        }
        $this->callbacks[] = array($callback, $callback);
        if (self::NORESULT !== $this->fired) {
            $this->process($this->result);
        }
    }

    /**
     * @param callback $callback Callback
     * @param callback $errback Error Callback
     */
    final public function addCallbacks($callback, $errback) {
        if (!is_callable($callback) || !is_callable($errback)) {
            throw new DieException('The contents of a variable can not be called as a function');
        }
        $this->callbacks[] = array($callback, $errback);
        if (self::NORESULT !== $this->fired) {
            $this->process($this->result);
        }
    }

    /**
     * @param mixed $result 
     */
    final public function callback($result) {
        $this->fired = self::SUCCESS;
        $this->process($result);
    }

    /**
     * @param mixed $result 
     */
    final public function errback($result) {
        $this->fired = self::ERROR;
        $this->process($result);
    }

    final private function process($result) {
        $this->result = $result;
        while (true) {
            if (is_a($this->result, 'Deferred')) {
                if ($this === $this->result) {
                    throw new DieException('Result in Deferred callbacks may not same Deferred instance');
                }
                $_this = $this;
                $this->result->addCallbacks(function($data) use($_this) {
                            $_this->callback($data);
                            return $data;
                        }, function($data) use($_this) {
                            $_this->errback($data);
                            return $data;
                        });
                break;
            }
            $callbacks = array_shift($this->callbacks);
            if (NULL === $callbacks) {
                break;
            }
            $callback = $callbacks[$this->fired];
            if (NULL === $callback) {
                continue;
            }
            try {
                $this->result = call_user_func($callback, $this->result);
            } catch (DieException $e) {
                $this->fired = self::ERROR;
            }
        }
    }
    
}